package server;

import java.io.IOException;
import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.RMISecurityManager;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.sql.SQLException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.PriorityQueue;
import java.util.Scanner;
import java.util.Timer;
import java.util.TimerTask;

import model.BuyOrder;
import model.Order;
import model.SellOrder;
import model.Stock;
import model.User;
import server.exception.InvalidOrderException;
import server.exception.LoginException;
import server.exception.RegistrationException;
import client.Client;

public class ServerImpl extends UnicastRemoteObject implements Server {
	private static final long serialVersionUID = 9082070010575397654L;
	private static final String DBNAME = "sadi.db";
	private static final int MAX_TRADE_SHARES = 10000;
	private static final int QUEUE_CHECK_RATE = 3000;
	private static final int SHARE_PRICE_DELTA = 10;
	
	// database access object
	private DataOperator dao;
	// all registered users, key user ID 
	private Map<Integer, User> userList;
	// all logged in users, key user ID
	private Map<Integer, Client> activeClients;
	// all stocks, key stock name
	private Map<String, Stock> stocks;
	// number of shares in each stock held by each user
	// key user ID, stock name
	private Map<Integer, Map<String, Integer>> holdings;
	// queues of waiting buy/sell orders, key stockname
	private Map<String, PriorityQueue<Order>> buy, sell;
	// mapping from transaction ID to Order for queued orders
	private Map<String, Order> activeTransactions;

	// scheduler for queue check thread
	private Timer queueTimer;
	
	// current highest transaction ID & lock, since can't synch on primitives
	private int maxTID;
	private Object tidLock = new Object();
	// format for transaction dates
	public final static DateFormat YEAR_MONTH_DAY = new SimpleDateFormat(
			"yyMMdd");

	public static void main(String args[]) throws SQLException,
			ClassNotFoundException, IOException {
		System.setSecurityManager(new RMISecurityManager());
		try {
			ServerImpl server = new ServerImpl();
			Naming.rebind("rmi://localhost:12345/ServerImpl", server);
			System.out.println("Created and registered ServerImpl object\nEnter q to exit.");
			// wait for q character to terminate server
			Scanner sc = new Scanner(System.in);
			while(sc.hasNext()) {
				String command = sc.nextLine();
				if(command.toLowerCase().charAt(0) == 'q') {
					// write out relevant in-memory data & shutdown
					server.terminate();
					UnicastRemoteObject.unexportObject(server, true);
					Naming.unbind("rmi://localhost:12345/ServerImpl");
					System.out.println("Server terminated.");
					break;
				}
			}
		} catch (RemoteException re) {
			System.err.println("Unable to add remote object to registry: "
					+ re.getMessage());
		} catch (MalformedURLException me) {
			System.err.println("Incorrect address for registry");
		} catch (Exception e) {
			System.err.println("error: " + e.getMessage());
			e.printStackTrace();
		}
	}

	/**
	 * Default constructor for the server
	 * 
	 * @throws SQLException
	 * @throws ClassNotFoundException
	 * @throws IOException
	 */
	protected ServerImpl() throws SQLException, ClassNotFoundException,
			IOException {
		super();

		// create a new instance of the database
		dao = new DataOperator(DBNAME);
		if (dao.dbLocked()) {
			System.err.println("DB in use, exiting");
			System.exit(1);
		}

		// update our storage in memory
		stocks = dao.getStocks();
		holdings = dao.getHoldings();
		// all users
		userList = dao.getUsers();
		// logged in users
		activeClients = new HashMap<Integer, Client>();
		activeTransactions = new HashMap<String, Order>();
		buy = new HashMap<String, PriorityQueue<Order>>();
		sell = new HashMap<String, PriorityQueue<Order>>();
		for (String sharename : stocks.keySet()) {
			buy.put(sharename, new PriorityQueue<Order>());
			sell.put(sharename, new PriorityQueue<Order>());
		}
		maxTID = dao.getMaxTID();

		queueTimer = new Timer();
		// every 3 seconds, check trades
		queueTimer.scheduleAtFixedRate(new TradeProcessTask(), 0,
				QUEUE_CHECK_RATE);
	}
	
	public void terminate() throws SQLException {
		// TODO probably should notify client of server term
		queueTimer.cancel();
		dao.saveUsers(userList.values());
		dao.saveHoldings(stocks, holdings);
		dao.saveStocks(stocks.values());
		dao.close();
	}
	
	/**
	 * Check at an interval if there are orders to process, and do so.
	 */
	class TradeProcessTask extends TimerTask {

		@Override
		public void run() {
			// check each stock being sold
			for (String stockname : sell.keySet()) {
				PriorityQueue<Order> sq = sell.get(stockname);
				// check each sell order
				for (Order sellOrder : sq) {
					try {
						// check for a matching buy order
						Order buyOrder = checkBuyers(stockname,
								sellOrder.getNumber(), sellOrder.getPrice());
						if (buyOrder != null) {
							processOrder(buyOrder, sellOrder);
						}
					} catch (RemoteException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
				}
			}
		}

	}
	
	/** Add a given number of shares to a user's holdings in a given stock */
	private void adjustHoldings(int userId, String stockname, int amount) {
		synchronized (holdings) {
			int had = holdings.get(userId).get(stockname);
			holdings.get(userId).put(stockname, had + amount);
		}
	}
	
	/** Process buy & sell orders and update stock price */
	private void processOrder(Order buyOrder, Order sellOrder) throws RemoteException {
		try {
			processBuyOrder(buyOrder, sellOrder);
			processSellOrder(sellOrder, buyOrder);
			// adjust share price
			updateStockPrice(buyOrder.getStockName(), buyOrder.getPrice());
		} catch (SQLException e) {
			throw new RemoteException("error processing order: " + e.getMessage());
		}
	}
	
	/** Complete a buy order and notify the buyer */
	private void processBuyOrder(Order buyOrder, Order sellOrder)
			throws RemoteException, SQLException {
		// remove buy order from queue & active
		PriorityQueue<Order> bq = buy.get(buyOrder.getStockName());
		bq.remove(buyOrder);
		activeTransactions.remove(buyOrder.getTransactionId());
		
		// notify buying client via callback
		int buyerId = buyOrder.getUserId();
		Client buyer = activeClients.get(buyerId);
		
		int remaining = buyOrder.getNumber() - sellOrder.getNumber();
		int origPrice = buyOrder.getPrice();
		if(sellOrder.getNumber() < buyOrder.getNumber()) {
			buyOrder.setNumber(sellOrder.getNumber());			
		}
		buyOrder.setPrice(sellOrder.getPrice());
		buyer.alertPurchase(buyOrder);
		
		// increment shares held by buyer
		adjustHoldings(buyerId, buyOrder.getStockName(), buyOrder.getNumber());
		// write completed trade to DB
		dao.completeTrade(buyOrder);

		// check if we bought all the shares we wanted
		if (remaining > 0) {
			Date now = new Date();
			String buyId = generateTransactionID(now);
			buyOrder = new BuyOrder(buyerId, buyId, now,
					buyOrder.getStockName(), remaining, origPrice);
			queueOrder(buy, buyOrder.getStockName(), buyOrder);
		}
	}
	
	/** Complete a sell order and notify the buyer */
	private void processSellOrder(Order sellOrder, Order buyOrder)
			throws RemoteException, SQLException {
		// remove sell order from queue & active
		PriorityQueue<Order> sq = sell.get(sellOrder.getStockName());
		sq.remove(sellOrder);
		activeTransactions.remove(sellOrder.getTransactionId());
		
		// notify selling client via callback
		int sellerId = sellOrder.getUserId();
		Client seller = activeClients.get(sellerId);
				
		int remaining = sellOrder.getNumber() - buyOrder.getNumber();
		int origPrice = sellOrder.getPrice();
		if(buyOrder.getNumber() < sellOrder.getNumber()) {
			sellOrder.setNumber(buyOrder.getNumber());			
		}
		sellOrder.setPrice(buyOrder.getPrice());
		seller.alertSold(sellOrder);
		
		// decrement shares held by buyer
		adjustHoldings(sellerId, sellOrder.getStockName(), -sellOrder.getNumber());
		// write completed trade to DB
		dao.completeTrade(sellOrder);
		
		// check if we sold all the shares we wanted
		if (remaining > 0) {
			Date now = new Date();
			String sellId = generateTransactionID(now);
			sellOrder = new SellOrder(sellerId, sellId, now,
					sellOrder.getStockName(), remaining, origPrice);
			queueOrder(sell, sellOrder.getStockName(), sellOrder);
		}
	}
	
	/** Add a new user to the system */
	public void registerUser(Client client, String username, String password)
			throws RemoteException, RegistrationException {
		// validate user name/password length requirements
		if (username == null || username.length() < User.MIN_USERNAME_LEN) {
			throw new RegistrationException(
					"username must be at least 8 characters");
		} else if (password == null
				|| password.length() < User.MIN_PASSWORD_LEN) {
			throw new RegistrationException(
					"password must be at least 8 characters");
		}

		try {
			int id = dao.registerUser(username, password);
			synchronized (userList) {
				// create user, add to user list & make active
				User u = new User(id, username, password);
				synchronized (activeClients) {
					activeClients.put(u.getId(), client);
				}
				userList.put(u.getId(), u);
				
				// set initial holdings (none)
				synchronized (holdings) {
					holdings.put(u.getId(), new HashMap<String, Integer>());
					for(Stock s : stocks.values()) {
						holdings.get(u.getId()).put(s.getName(), 0);
					}
				}
				
				// notify client of successful registration
				client.alertRegistered(u);
			}
		} catch (SQLException e) {
			throw new RegistrationException("User with that name already exists");
		}
	}

	/** Login an existing user */
	public void loginUser(Client client, String username, String password)
			throws RemoteException, LoginException {
		// check user exists
		for (User u : userList.values()) {
			if (u.getUsername().equals(username)
					&& u.getPassword().equals(password)) {
				client.setLoggedIn(u);
				synchronized (activeClients) {
					activeClients.put(u.getId(), client);
					return;
				}
			}
		}
		throw new LoginException("Could not login with those details!");
	}

	/**
	 * Retrieve details of a stock
	 * 
	 * @param stockname
	 *            Name of stock to retrieve
	 * @return Stock object
	 * @throws RemoteException
	 */
	public Stock getStock(String stockname) throws RemoteException {
		if (stockname == null) {
			throw new RemoteException("invalid stockname");
		}
		synchronized (stocks) {
			return stocks.get(stockname);
		}
	}

	/** Check order details meet system requirements for number & price */
	private boolean validateOrder(String stockname, int number, int price)
			throws InvalidOrderException, RemoteException {
		Stock stock = getStock(stockname);
		// check stock exists
		if (stock == null)
			throw new InvalidOrderException("no stock with that name");

		// check not at share limit
		if (number > MAX_TRADE_SHARES)
			throw new InvalidOrderException("too many shares requested");

		// check request price is +- 10
		if (price < stock.getPrice() - SHARE_PRICE_DELTA
				|| price > stock.getPrice() + SHARE_PRICE_DELTA)
			throw new InvalidOrderException(
					"order not within price restrictions");
		
		return true;
	}

	/** Set the price of a given stock */
	private void updateStockPrice(String stockname, int price) {
		synchronized (stocks) {
			Stock s = stocks.get(stockname);
			if(s != null) {
				s.setPrice(price);
			}
		}
	}

	/**
	 * Attempt to find a SellOrder matching given parameters.
	 * 
	 * @param stockname
	 *            Name of stock.
	 * @param number
	 *            Number to order
	 * @param price
	 *            Price to order at
	 * @return First order found according to PriorityQueue ordering, or null if
	 *         none.
	 */
	private Order checkSellers(String stockname, int number, int price) {
		synchronized (sell) {
			PriorityQueue<Order> pq = sell.get(stockname);
			// check there are sellers for this stock
			if (!pq.isEmpty()) {
				// check each seller for price
				for (Order o : pq) {
					// if no more than our requested price, ok to buy
					if (o.getPrice() <= price) {
						return o;
					}
				}
			}
			return null;
		}
	}

	/**
	 * Attempt to find a BuyOrder matching given parameters.
	 * 
	 * @param stockname
	 *            Name of stock.
	 * @param number
	 *            Number to order
	 * @param price
	 *            Price to order at
	 * @return First order found according to PriorityQueue ordering, or null if
	 *         none.
	 */
	private Order checkBuyers(String stockname, int number, int price) {
		synchronized (buy) {
			PriorityQueue<Order> pq = buy.get(stockname);
			// check there are sellers for this stock
			if (!pq.isEmpty()) {
				// check each seller for price
				for (Order o : pq) {
					// if no more than our requested price, ok to buy
					if (o.getPrice() >= price) {
						return o;
					}
				}
//				System.err.println("no price match for " + stockname);
//			} else {
//				System.err.println("buy queue empty");
			}
			return null;
		}
	}

	/**
	 * Attempt to purchase stock. May be completed immediately or queued until
	 * an offer appears.
	 */
	public void buyStock(Client client, String stockname, int number, int price)
			throws RemoteException, InvalidOrderException {
		User u = client.getUser();
		if (!isValidUser(u)) {
			throw new RemoteException("incorrect login");
		}

		// create a new buy order
		Date now = new Date();
		String buyId = generateTransactionID(now);
		Order buyOrder = new BuyOrder(u.getId(), buyId, now, stockname,
				number, price);
		// ensure it's acceptable
		if (validateOrder(stockname, number, price)) {
			// check if there are buy offers for this stock
			Order sellOrder = checkSellers(stockname, number, price);
			// if we can buy immediately, do so
			if (sellOrder != null) {
				processOrder(buyOrder, sellOrder);
				// other wise, queue the order
			} else {
				queueOrder(buy, stockname, buyOrder);
			}
		}
	}
	
	/**
	 * Attempt to sell stock. May be completed immediately or queued until
	 * an offer appears.
	 */
	public void sellStock(Client client, String stockname, int number, int price)
			throws RemoteException, InvalidOrderException {
		User u = client.getUser();
		if (u == null) {
			throw new InvalidOrderException("must be logged in to sell");
		}
		if (!isValidUser(u)) {
			throw new RemoteException("incorrect login");
		}

		Date now = new Date();
		String sellId = generateTransactionID(now);
		Order sellOrder = new SellOrder(u.getId(), sellId, now,
				stockname, number, price);
		if (validateOrder(stockname, number, price))  {
			// check user has this many shares to process
			if(holdings.get(u.getId()).get(stockname) < number) 
				throw new InvalidOrderException("you don't hold enough shares for that");
			
			// check if there are buy offers for this stock
			Order buyOrder = checkBuyers(stockname, number, price);
			if (buyOrder != null) {
				processOrder(buyOrder, sellOrder);
			} else {
				queueOrder(sell, stockname, sellOrder);
			}
		}
	}

	/** add an order to the relevant queue */
	private void queueOrder(Map<String, PriorityQueue<Order>> queue,
			String stockname, Order order) {
		activeTransactions.put(order.getTransactionId(), order);
		synchronized (queue) {
			PriorityQueue<Order> pq = queue.get(stockname);
			pq.add(order);
		}
	}

	/** Return a new Transaction ID */
	private String generateTransactionID(Date d) {
		int tid;
		synchronized (tidLock) {
			tid = maxTID;
			maxTID++;
		}
		return String.format("TR%s%06d", YEAR_MONTH_DAY.format(d), tid);
	}

	@Override
	/** Cancel a sale order with the given transaction ID */
	public void cancelSell(Client client, String transactionID)
			throws RemoteException {
		User u = client.getUser();
		if(!isValidUser(u)) {
			throw new RemoteException("invalid credentials");
		}
		Order o = activeTransactions.get(transactionID);
		if(o != null) {
			removeOrder(sell, o.getStockName(), o.getTransactionId(), u.getId());
		}
	}

	@Override
	/** Cancel a sale order with the given transaction ID */
	public void cancelBuy(Client client, String transactionID)
			throws RemoteException {
		User u = client.getUser();
		if(!isValidUser(u)) {
			throw new RemoteException("invalid credentials");
		}
		Order o = activeTransactions.get(transactionID);
		if(o != null) {
			removeOrder(buy, o.getStockName(), o.getTransactionId(), u.getId());
		}
	}

	/** Attempt to remove an order from the given order queue */
	private void removeOrder(Map<String, PriorityQueue<Order>> orders,
			String stockname, String transactionID, int userId)
			throws RemoteException {
		synchronized (orders) {
			PriorityQueue<Order> sq = orders.get(stockname);
			if (sq != null) {
				for (Order o : sq) {
					if (o.getTransactionId().equals(transactionID)) {
						if (o.getUserId() == userId) {
							synchronized (activeTransactions) {
								activeTransactions.remove(transactionID);
							}
							sq.remove(o);
							return;
						} else {
							throw new RemoteException(
									"not your order to cancel");
						}
					}
				}
			}
		}
	}
	
	/** Return if given user's details match a registered user */ 
	private boolean isValidUser(User u) {
		User check = userList.get(u.getId());
		if (check == null)
			return false;
		return check.getUsername().equals(u.getUsername())
				&& check.getPassword().equals(u.getPassword());
	}

	@Override
	/** Return a list of all stocks */
	public ArrayList<Stock> getStocks(Client client) throws RemoteException {
		User u = client.getUser();
		if(!isValidUser(u)) {
			throw new RemoteException("invalid credentials");
		}
		return new ArrayList<Stock>(stocks.values());
	}

	@Override
	/** Return a list of all sell offers for a given stock */
	public ArrayList<Order> getSellOffers(Client client, String stockname)
			throws RemoteException {
		User u = client.getUser();
		if(!isValidUser(u)) {
			throw new RemoteException("invalid credentials");
		}
		return ordersList(sell, stockname);
	}

	@Override
	/** Return a list of all buy offers for a given stock */
	public ArrayList<Order> getBuyOffers(Client client, String stockname)
			throws RemoteException {
		User u = client.getUser();
		if(!isValidUser(u)) {
			throw new RemoteException("invalid credentials");
		}
		return ordersList(buy, stockname);
	}

	/** Get a list of orders in a the queue */
	private ArrayList<Order> ordersList(
			Map<String, PriorityQueue<Order>> orders, String stockname) {
		PriorityQueue<Order> offers = orders.get(stockname);
		if (offers == null || offers.size() == 0) {
			return null;
		}
		return new ArrayList<Order>(offers);
	}
	
	/** Return all of a user's active orders */
	public ArrayList<Order> getUserOrders(Client client) throws RemoteException {
		User u = client.getUser();
		if(!isValidUser(u)) {
			throw new RemoteException("invalid credentials");
		}
		
		ArrayList<Order> orders = new ArrayList<Order>();
		for(Order o : activeTransactions.values()) {
			if(o.getUserId() == u.getId()) {
				orders.add(o);
			}
		}
		return orders;
	}
	
	/** Return all orders for a user between the given dates */
	public ArrayList<Order> getUserHistory(Client client, Date a, Date b) throws RemoteException {
		// check client login
		User u = client.getUser();
		if(!isValidUser(u)) {
			throw new RemoteException("invalid credentials");
		}
		try {
			return dao.getUserHistory(u.getId(), a, b);
		} catch (SQLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return null;
	}

	@Override
	/** Log user out of server & abort their incomplete orders */
	public void logoutUser(Client client) throws RemoteException {
		User u = client.getUser();
		synchronized (activeClients) {
			if (activeClients.containsKey(u.getUsername())) {
				// remove from active clients
				activeClients.remove(u.getUsername());
				// discard any incomplete orders
				for (Order o : getUserOrders(client)) {
					if (o instanceof BuyOrder) {
						cancelBuy(client, o.getTransactionId());
					} else {
						cancelSell(client, o.getTransactionId());
					}
				}
			}
		}
	}

	@Override
	/** Return how many shares a user holds of a given stock */
	public Integer getHolding(Client client, String stockname)
			throws RemoteException {
		// check client login
		User u = client.getUser();
		if(!isValidUser(u)) {
			throw new RemoteException("invalid credentials");
		}
		synchronized (holdings) {
			return holdings.get(u.getId()).get(stockname);	
		}
	}
}
